package com.atguigu.gulimall.search.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atguigu.common.to.SkuSearchVo;
import com.atguigu.common.utils.R;
import com.atguigu.gulimall.search.config.GulimallElasticSearchConfig;
import com.atguigu.gulimall.search.constant.SearchConstant;
import com.atguigu.gulimall.search.feign.ProductFeignService;
import com.atguigu.gulimall.search.service.MallSearchService;
import com.atguigu.gulimall.search.vo.AttrResponseVo;
import com.atguigu.gulimall.search.vo.BrandVo;
import com.atguigu.gulimall.search.vo.SearchParam;
import com.atguigu.gulimall.search.vo.SearchResult;
import org.apache.commons.lang.StringUtils;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.RangeQueryBuilder;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.nested.ParsedNested;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.io.IOException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;

@Service
public class MallSearchServiceImpl implements MallSearchService {

    @Resource
    private RestHighLevelClient client;

    @Resource
    private ProductFeignService productFeignService;

    @Override
    public SearchResult search(SearchParam searchParam) throws IOException {

        SearchRequest searchRequest = buildSearchRequest(searchParam);

        SearchResponse searchResponse = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
        SearchResult result = buildSearchResult(searchResponse,searchParam);

        return result;
    }

    private SearchResult buildSearchResult(SearchResponse searchResponse,SearchParam searchParam) {
        SearchHit[] hits = searchResponse.getHits().getHits();
        SearchResult result = new SearchResult();
        ArrayList<SkuSearchVo> skuSearchVos = new ArrayList<>(hits.length);
        for (int i = 0; i < hits.length; i++) {
            SkuSearchVo skuSearchVo = JSON.parseObject(hits[i].getSourceAsString(), SkuSearchVo.class);
            //设置高亮
            if (StringUtils.isNotBlank(searchParam.getKeyword())){
                skuSearchVo.setSkuTitle(hits[i].getHighlightFields().get("skuTitle").getFragments()[0].toString());
            }
            skuSearchVos.add(skuSearchVo);
        }
        result.setProducts(skuSearchVos);

        ParsedLongTerms catalogIdAgg = searchResponse.getAggregations().get("catalogId_agg");
        ArrayList<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
        for (Terms.Bucket bucket : catalogIdAgg.getBuckets()) {
            Long catalogId = (Long) bucket.getKey();
            ParsedStringTerms catalogNameAgg = bucket.getAggregations().get("catalogName_agg");
            String catalogName = (String)catalogNameAgg.getBuckets().get(0).getKey();
            SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
            catalogVo.setCatalogId(catalogId);
            catalogVo.setCatalogName(catalogName);
            catalogVos.add(catalogVo);
        }
        result.setCatalogs(catalogVos);

        ParsedLongTerms brandIdAgg = searchResponse.getAggregations().get("brandId_agg");
        ArrayList<SearchResult.BrandVo> brandVos = new ArrayList<>();
        for (Terms.Bucket bucket : brandIdAgg.getBuckets()) {
            Long brandId = (Long) bucket.getKey();
            ParsedStringTerms brandNameAgg = bucket.getAggregations().get("brandName_agg");
            String brandName = (String)brandNameAgg.getBuckets().get(0).getKey();
            ParsedStringTerms brandImgAgg = bucket.getAggregations().get("brandImg_agg");
            String brandImg = (String)brandImgAgg.getBuckets().get(0).getKey();
            SearchResult.BrandVo brandVo = new SearchResult.BrandVo();
            brandVo.setBrandId(brandId);
            brandVo.setBrandName(brandName);
            brandVo.setBrandImg(brandImg);
            brandVos.add(brandVo);
        }
        result.setBrands(brandVos);

        ParsedNested attrAgg = searchResponse.getAggregations().get("attr_agg");
        ParsedLongTerms attrIdAgg = attrAgg.getAggregations().get("attrId_agg");
        ArrayList<SearchResult.AttrVo> attrVos = new ArrayList<SearchResult.AttrVo>();
        for (Terms.Bucket bucket : attrIdAgg.getBuckets()) {
            SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
            Long attrId = (Long) bucket.getKey();
            ParsedStringTerms attrNameAgg = bucket.getAggregations().get("attrName_agg");
            String attrName = (String) attrNameAgg.getBuckets().get(0).getKey();
            attrVo.setAttrId(attrId);
            attrVo.setAttrName(attrName);
            ParsedStringTerms attrValueAgg = bucket.getAggregations().get("attrValue_agg");
            ArrayList<String> attrValues = new ArrayList<>();
            for (Terms.Bucket attrValueAggBucket : attrValueAgg.getBuckets()) {
                String attrValue = (String) attrValueAggBucket.getKey();
                attrValues.add(attrValue);
            }
            attrVo.setAttrValue(attrValues);
            attrVos.add(attrVo);
        }
        result.setAttrs(attrVos);


        long totalHits = searchResponse.getHits().getTotalHits().value;
        result.setTotal(totalHits);
        long totalPages = totalHits % SearchConstant.PAGE_SIZE > 0 ?
                totalHits / SearchConstant.PAGE_SIZE + 1 :
                totalHits / SearchConstant.PAGE_SIZE;
        result.setTotalPages(
                totalPages
                );
        result.setPageNum(searchParam.getPageNum());

        ArrayList<Integer> pageNavs = new ArrayList<>();
        for (int i = 1; i <= totalPages; i++) {
            pageNavs.add(i);
        }

        result.setPageNavs(pageNavs);

        ArrayList<SearchResult.NavVo> navVos = new ArrayList<>();
        List<String> attrs = searchParam.getAttrs();
        Set<Long> attrIdSet = new HashSet<>();
        if (attrs != null && attrs.size() > 0){
            for (String attr : attrs) {
                SearchResult.NavVo navVo = new SearchResult.NavVo();
                String[] split = attr.split("_");
                Long attrId = Long.parseLong(split[0]);
                //TODO 这里循环调用远程服务查数据库，还需优化
                R info = productFeignService.info(attrId);
                if (info.getCode() == 0){
                    AttrResponseVo data = info.getData(new TypeReference<AttrResponseVo>() {
                    },"attr");
                    navVo.setNavName(data.getAttrName());
                }else{
                    navVo.setNavName(split[0]);
                }
                attrIdSet.add(attrId);
                String navValues = split[1];
                navVo.setNavValue(navValues);

                navVo.setLink(getLink(searchParam,"attrs",attr));
                navVos.add(navVo);
            }
        }

        List<Long> brandIds = searchParam.getBrandId();
        if (brandIds != null && brandIds.size() > 0){
            //先根据brandId查出brandName
            List<BrandVo> brandVoList = new ArrayList<>();
            R info = productFeignService.getBrandsByIds(brandIds);
            if (info.getCode() == 0){
                brandVoList = info.getData(new TypeReference<List<BrandVo>>() {
                }, "brands");
            }

            for (BrandVo brandVo : brandVoList) {
                SearchResult.NavVo navVo = new SearchResult.NavVo();
                navVo.setNavName("品牌");
                navVo.setNavValue(brandVo.getName());
                navVo.setLink(getLink(searchParam,"brandId",brandVo.getBrandId()));
                navVos.add(navVo);
            }

        }

        //如果已经筛选了某个属性，则属性选择中不再添加这项属性
        List<SearchResult.AttrVo> attrs1 = result.getAttrs();
        List<SearchResult.AttrVo> attrVos1 = attrs1.stream()
                .filter(item->!attrIdSet.contains(item.getAttrId()))
                .collect(Collectors.toList());
        result.setAttrs(attrVos1);

        result.setNavs(navVos);

        return result;
    }

    private static String getLink(SearchParam searchParam, String key,Object value) {
        //TODO 这里浏览器和后端对空格等字符的base64编码不一样，可能还有问题
        String s = URLDecoder.decode(searchParam.getQueryString()).replaceAll("&?"+key+"=" + value, "");
        return "http://search.gulimall.com/list.html?"+s;
    }

    private static SearchRequest buildSearchRequest(SearchParam searchParam) {
        SearchRequest searchRequest = new SearchRequest();
        searchRequest.indices(SearchConstant.PRODUCT_INDEX);
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        if (StringUtils.isNotBlank(searchParam.getKeyword())) {
            boolQueryBuilder = boolQueryBuilder.must(QueryBuilders.matchQuery("skuTitle", searchParam.getKeyword()));
        }
        if (searchParam.getCatalog3Id() != null) {
            boolQueryBuilder = boolQueryBuilder.
                    filter(QueryBuilders.termQuery("catalogId", searchParam.getCatalog3Id()));
        }
        if (searchParam.getHasStock() != null) {
            boolQueryBuilder = boolQueryBuilder.
                    filter(QueryBuilders.termQuery("hasStock",
                            searchParam.getHasStock().intValue() == 1));
        }
        String skuPrice = searchParam.getSkuPrice();
        double minPrice = 0;
        double maxPrice = 0;
        if (StringUtils.isNotBlank(skuPrice) && skuPrice.contains("_")) {
            String[] split = skuPrice.split("_");
            if (split.length == 1) {
                if (skuPrice.startsWith("_")) {
                    maxPrice = Double.parseDouble(skuPrice.substring(1));
                }
                if (skuPrice.endsWith("_")) {
                    minPrice = Double.parseDouble(skuPrice.substring(0, skuPrice.length() - 1));
                }
            } else if (split.length == 2) {
                maxPrice = Double.parseDouble(split[1]);
                minPrice = Double.parseDouble(split[0]);
            } else {
                throw new RuntimeException("价格格式有误");
            }
        }

        RangeQueryBuilder priceRangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
        //TODO 浮点类型不能这么比较大小吧
        if (minPrice > 0) {
            priceRangeQueryBuilder = priceRangeQueryBuilder.gte(minPrice);
        }
        if (maxPrice > 0) {
            priceRangeQueryBuilder = priceRangeQueryBuilder.lte(minPrice);
        }
        boolQueryBuilder = boolQueryBuilder.filter(priceRangeQueryBuilder);

        if (searchParam.getBrandId() != null && searchParam.getBrandId().size() > 0) {
            boolQueryBuilder = boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId", searchParam.getBrandId()));
        }

        //TODO 格式校验逻辑线先不写。后续应该写在Controller层？或者用自定义校验注解？
        List<String> attrs = searchParam.getAttrs();
        if (attrs != null && attrs.size() > 0) {
            for (String item : attrs) {
                String[] split = item.split("_");
                Long attrId = Long.parseLong(split[0]);
                String[] values = split[1].split(":");
                boolQueryBuilder = boolQueryBuilder.filter(QueryBuilders.
                        nestedQuery("attrs", QueryBuilders.boolQuery().
                                        must(QueryBuilders.termQuery("attrs.attrId", attrId)
                                        ).must(QueryBuilders.termsQuery("attrs.attrValue", values)),
                                ScoreMode.None));
            }
        }


        SearchSourceBuilder sourceBuilder = searchSourceBuilder.query(boolQueryBuilder);

        //还要再加上排序、高亮、分页
        String sortParam = searchParam.getSort();
        if (StringUtils.isNotBlank(sortParam)) {
            String[] split = sortParam.split("_");
            sourceBuilder = sourceBuilder.sort(split[0],
                    StringUtils.equalsIgnoreCase(split[1], "desc") ? SortOrder.DESC : SortOrder.ASC);
        }

        if (StringUtils.isNotBlank(searchParam.getKeyword())) {
            HighlightBuilder highlightBuilder = new HighlightBuilder()
                    .field("skuTitle")
                    .preTags("<b style='color:red'>")
                    .postTags("</b>");
            sourceBuilder = sourceBuilder.highlighter(highlightBuilder);
        }

        //分页
        sourceBuilder.from((searchParam.getPageNum()-1)*SearchConstant.PAGE_SIZE);
        sourceBuilder.size(SearchConstant.PAGE_SIZE);

        //聚合品牌
        sourceBuilder = sourceBuilder.aggregation(AggregationBuilders.terms("brandId_agg").field("brandId")
                .subAggregation(AggregationBuilders.terms("brandName_agg").field("brandName"))
                .subAggregation(AggregationBuilders.terms("brandImg_agg").field("brandImg")));

        //聚合分类catalog
        sourceBuilder = sourceBuilder.aggregation(AggregationBuilders.terms("catalogId_agg").field("catalogId")
                .subAggregation(AggregationBuilders.terms("catalogName_agg").field("catalogName")));

        //属性attr聚合
        sourceBuilder = sourceBuilder.aggregation(AggregationBuilders.nested("attr_agg","attrs")
                .subAggregation(AggregationBuilders.terms("attrId_agg").field("attrs.attrId")
                        .subAggregation(AggregationBuilders.terms("attrName_agg").field("attrs.attrName"))
                        .subAggregation(AggregationBuilders.terms("attrValue_agg").field("attrs.attrValue"))));


        System.out.println(sourceBuilder);
        searchRequest.source(sourceBuilder);
        return searchRequest;
    }
}
